const express = require('express'),
    errorhandler = require('errorhandler');
const fs = require("fs");
const path = require("path");
const serveIndex = require('serve-index');
const handlers = require('./handlers');
const cache = require('./cacheUtil');
const {hashLink, escapeUrl} = require("./util");

const isProduction = process.env.NODE_ENV === 'production';

// Create global app object
let app = express();
app.all('*',function (req, res, next) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials");
  // res.setHeader("Content-Type", "application/json;charset=utf-8");
   next();
});



const host = "localhost";
const port = process.env.PORT || 3010;
// const port = process.env.PORT || 3010;

const startPathMap = {
    // "Clips": "E:/diskstation/03_Clips",
    // "Videos": "E:/diskstation/03_Clips_video",
    "WebArchivesInbox": "E:/03.WebArchivesInbox/inbox",
    // "WebArchivesVideoSeriesInbox": "//diskstation2/4096G_02/03.WebArchivesInboxVideoSeries",
    // "WebArchivesResources": "//diskstation2/4096G_02/03.WebArchivesResources",
    "WebArchivesVideoInbox": "E:/03.WebArchivesInboxVideo/inbox",
    // "WebArchivesVault": "E:/WebArchivesVault",
    "Temp": "E:/diskstation/SingleFileWebArchives",
};
const cacheEnabledMap = {
    "Temp": () => constantlyFalse("Temp"),
    "WebArchivesInbox": () => compareFolderModification("WebArchivesInbox"),
    // "WebArchivesVideoSeriesInbox": () => compareFolderModification("WebArchivesVideoSeriesInbox"),
    // "WebArchivesResources": () => compareFolderModification("WebArchivesResources"),
    // "WebArchivesVideoInbox": () => compareFolderModification("WebArchivesVideoInbox"),
}

const constantlyFalse = (name) => {
  return false;
}

// 当往实际目录写入了一个新文件后，会导致实际目录modifiedDatetime更新
// cache文件最后的更新时间比实际目录落后了，那么就要触发重新更新cache
const compareFolderModification = (name) => { // name="WebArchivesInbox"
  if (!fs.existsSync(startPathList[name])){
    return false; // 如果目录不存在就disable cache，因为启用cache也没有用，最后文件还是访问不到
  }
  const targetFolderStat = fs.statSync(startPathMap[name]);
  const cacheFileStat = fs.statSync(`./data_${name}.json`);
  const timeDiff = targetFolderStat.mtime - cacheFileStat.mtime; // 目标目录更新时间比cache文件后面超过10分钟，则要重新刷新缓存
  const minutesDiff = timeDiff / (1000 * 60); // 转换为分钟
  console.log(`refresh ${name}, time diff ${minutesDiff} minutes.`)
  const over10minutes =  minutesDiff >= 10;
  return over10minutes ? false : true;  // 大于10分钟就disable cache
};

const startPathNameMap = new Map(Array.from(cache.objToStrMap(startPathMap), a => a.reverse()));
let startPathList = Array.from(new Map(Object.entries(startPathMap)).values());  // JM: the real start point to travel

const includeExtMap = {
    "html": handlers.htmlHandler,
    "htm": handlers.htmlHandler,
    "mhtml": handlers.htmlHandler,
    "jpg": handlers.htmlHandler,
    "jpeg": handlers.htmlHandler,
    "png": handlers.htmlHandler,
    "svg": handlers.htmlHandler,
    "pdf": handlers.htmlHandler,
    "folder": handlers.folderHandler,
    "mp3": handlers.mediaFileHandler,
    "mp4": handlers.mediaFileHandler,
    "default": handlers.htmlHandler
}
const includeExtList = Array.from(new Map(Object.entries(includeExtMap)).keys());

const contentTypeMap = {
  "html": 'text/html; charset=utf-8',
  "htm": 'text/html; charset=utf-8',
  "mhtml": 'multipart/related',
  "jpeg": 'image/jpeg',
  "jpg": 'image/jpeg',
  "png": 'image/png',
  "svg": 'image/svg+xml',
  "mp3": "audio/mpeg",
  "mp4": 'video/mp4',
  "pdf": 'application/pdf',
  "folder": 'text/html; charset=utf-8',
  "default": 'text/html; charset=utf-8' // application/octet-stream
}
const contentTypeList = Array.from(new Map(Object.entries(contentTypeMap)).keys());
const excludeFolderList = [
  "XYplorer_23.20.0100_Portable",
]

const home = `http://${host}:${port}`

// app.locals.map = map;
app.locals.includeExtList = includeExtList;
app.locals.contentTypeMap = contentTypeMap;
app.locals.contentTypeList = contentTypeList;

if (!isProduction) {
  app.use(errorhandler());
}

function travel(dir,callback){
  const parentDir = dir.replaceAll("\\", "/").split("/").slice(-1)[0];
  // if (excludeFolderList.includes(dir.replaceAll("\\", "/"))){
  if (excludeFolderList.includes(parentDir)){
    return;
  }
  if (!fs.existsSync(dir)) {
    return;
  }
  fs.readdirSync(dir).forEach((file)=>{
    const pathname=path.join(dir,file)
    if(fs.statSync(pathname).isDirectory() && pathname.indexOf("__") === -1){ // 如果文件夹中包含__这个特征符号，那表示他也是可以被路由的
      travel(pathname,callback);
    }else{
      callback(pathname);
    }
  })
}
function scanLocalCacheFile() {
    let cacheFileList = [];
    fs.readdirSync('./').forEach(file => {
        // console.log(file);
        if (file.startsWith("data_")) {
            console.log(`found cache file ${file}`);
            cacheFileList.push(file);
        }
    });
    // let map = new Map();
    let startPathListExceptCache = startPathList;
    if (cacheFileList.length > 0) {
        cacheFileList.forEach((value, index) => {
            const [fixed, cacheName] = path.basename(value, ".json").split("_");
            let cacheEnabled = cacheEnabledMap.hasOwnProperty(cacheName) ? cacheEnabledMap[cacheName]() : true;
            console.log(`cache ${cacheName} enabled: ${cacheEnabled}`)
            if (cacheEnabled) {
                // JM: delete from startPathList, no need re-index from disk.
                startPathListExceptCache = startPathListExceptCache.filter(x => x !== startPathMap[cacheName]);
                // JM: start to read cache to memory.
                let obj = JSON.parse(fs.readFileSync(`./${value}`, 'utf-8'));
                let tempMap = cache.objToStrMap(obj);
                map = new Map([...map, ...tempMap]);
            }
        });
    }
    return startPathListExceptCache;
}

function scanLocalFolderAndInitialize(startPathListExceptCache, duplicatedList) {
    // scan local folder and initialize
    startPathListExceptCache.forEach((value, index) => {
        let tempMap = new Map(); // 一个tempMap代表一个目录
        const cacheName = startPathNameMap.get(value);
        travel(value,function(pathname){
            const basename = path.basename(pathname);
            const hashWithExt = basename.split("__").slice(-1)[0];
            const hash = hashWithExt.substring(0, hashWithExt.lastIndexOf("."));
            const ext = basename.substring(basename.lastIndexOf('.') + 1);
            const htmlDesc = basename.split("__").slice(0)[0];
            const ctime = fs.statSync(pathname)["ctimeMs"];

            if (basename.indexOf("__") !== -1 && includeExtList.includes(ext)) { // TODO: 这里要检查的更严格一些，哪些资源文件需要被加入索引，哪些不用
                if (tempMap.has(hashWithExt) && duplicatedList){
                    duplicatedList.push({"pathname": pathname, "hashWithExt": hashWithExt, "hash": hash, "ext": ext, "htmlDesc": htmlDesc, "ctime": ctime, "dupwith": tempMap.get(hashWithExt)});
                    console.warn(`duplicated found ${pathname}`);
                } else {
                    console.log(pathname)
                }
                tempMap.set(hashWithExt, {"pathname": pathname, "hashWithExt": hashWithExt, "hash": hash, "ext": ext, "htmlDesc": htmlDesc, "ctime": ctime});
            }
            if (ext === "folder") {
              const dir = pathname;
              fs.readdirSync(dir).forEach((file)=>{
                const pathname = path.join(dir,file);
                if(fs.statSync(pathname).isFile()) {
                  // const folderHashWithExt = path.basename(dir).split("__").slice(-1)[0];
                  // const folderHash = folderHashWithExt.substring(0, folderHashWithExt.lastIndexOf("."));
                  const fileBasename = escapeUrl(path.basename(pathname));
                  const fileHashWithExt = `${hash}_${fileBasename}`;
                  const fileHash = fileHashWithExt.substring(0, fileHashWithExt.lastIndexOf("."));
                  const fileExt = fileBasename.substring(fileBasename.lastIndexOf('.') + 1);
                  tempMap.set(fileHashWithExt, {"pathname": pathname, "hashWithExt": fileHashWithExt, "hash": fileHash, "ext": fileExt, "htmlDesc": htmlDesc, "ctime": ctime});
                }
              })
            }
        });
        // cache for one start path.
        let cacheEnabled = cacheEnabledMap.hasOwnProperty(cacheName) ? cacheEnabledMap[cacheName]() : true;
        if (!cacheEnabled) {
            fs.writeFileSync(`./data_${cacheName}.json`, JSON.stringify(cache.strMapToObj(tempMap), null, 2), { encoding: 'utf-8'});
        }
        // concat two maps.
        map = new Map([...map, ...tempMap]);
    });
// fs.writeFileSync('./data.json', JSON.stringify(cache.strMapToObj(map), null, 2) , 'utf-8');
}

function assembleResourceRouter(router, map) {
// assemble the resource type router. e.g. <desc>_<hash>.html, *.mhtml, *.mp4
    contentTypeList.forEach((v, i, ary) => {
        router.get(`/*.folder/*.${v}`, (req, res, next) => {
          // JM：这里要和下面的路由配合起来用，先拦截由serveIndex发起的folder下的某个文件的请求，
          // 形式上变为下面*__*这种形式，然后再由下面这条路由最终转换成map.forEach中注册的路由。
          const pathname = decodeURIComponent(req.path);
          const dirname = path.dirname(pathname).split("/").slice(-1)[0];
          const basename = path.basename(pathname);
          const hash = dirname.replaceAll(".folder", "");
          const fn = escapeUrl(basename);
          const hashWithExt = `${hash}_${fn}`;
          res.redirect(`${home}/${hashWithExt}`);
        })
        router.get(`/*__*.${v}`, (req, res, next) => {
          // JM：这个是将物理地址转换为下面的虚拟地址，比如拍入浏览器的是http://localhost:3010/「ReactNative原理」启动流程 - 掘金__adb6c10c4d0e044afebdaa0f62c6c352599c5ad0.html，将被转换为http://localhost:3010/adb6c10c4d0e044afebdaa0f62c6c352599c5ad0#~:~hint=xxx
          const pathname = req.path;
          const hashWithExt = pathname.split("__").slice(-1)[0];
          const hash = hashWithExt.substring(0, hashWithExt.lastIndexOf("."));
          const htmlDesc = path.basename(pathname.split("__").slice(0)[0]);
          res.redirect(`${home}/${hashWithExt}#~:~hint=${htmlDesc}`);
        })
    });

// assemble the resource file router. e.g. <hash>.html
    map.forEach((v, k, map) => {
        const physicsPath = v.pathname;
        const hashWithExt = v.hashWithExt;
        const hash = v.hash;
        const ext = v.ext;
        const desc = v.htmlDesc;
        // const routeMethod = ext === "mp4" ? routeMethods.mediaRoute : routeMethods.htmlRoute;
        let handlerName = ext;
        if (!includeExtMap.hasOwnProperty(handlerName)) {
          handlerName = "default";
        }
        const handler = includeExtMap[handlerName]; // 这里必须要把handler拿出来，否则不知什么原因会走到jpg那个handler上去
        router.get(`/${encodeURIComponent(hashWithExt)}`, handler);
    });
}

let router = require('express').Router();
let map = new Map();
let startPathListExceptCache = scanLocalCacheFile();
scanLocalFolderAndInitialize(startPathListExceptCache);
assembleResourceRouter(router, map);
fs.watch(startPathMap["Temp"], (eventType, filename) => {
    map = new Map();
    startPathListExceptCache = scanLocalCacheFile();
    scanLocalFolderAndInitialize(startPathListExceptCache);
    assembleResourceRouter(router, map);
    app.locals.map = map;
})
app.locals.map = map;

router.get('/reload', (req, res, next) => {
    map = new Map();
    startPathListExceptCache = scanLocalCacheFile();
    scanLocalFolderAndInitialize(startPathListExceptCache);
    assembleResourceRouter(router, map);
    app.locals.map = map;
    res.redirect('./');
});

router.get('/statistic', (req, res, next) => {
  return res.json(map.size);
});
function excludeFolder(filename, index, files, dir){
  return !excludeFolderList.includes(filename);
}
router.get('/', (req, res, next) => {
    let content = ""
    new Map(Object.entries(startPathMap)).forEach((value, key, map) => {
        const item = `<li><a href="/${key}">${value}</a></li>`;
        content += item;
    });
    res.send(`
<html>
    <li><a href="/latest">Latest</a></li>
    <li><a href="/desc">Desc</a></li>
    <li><a href="/reload">Reload</a></li>
    <li><a href="/duplicated">Duplciated Check</a></li>
    ${content}
</html>
    `);
});
new Map(Object.entries(startPathMap)).forEach((value, key, map) =>{
    app.use(`/${key}`, serveIndex(value, {'icons': true, "filter": excludeFolder, 'view': "details"}));
});

router.get('/duplicated', (req, res, next) => {
    let content = "";
    let duplicatedList = [];
    scanLocalFolderAndInitialize(startPathList, duplicatedList)
    duplicatedList.forEach((v, i, ary) => {
        const pathname = v.pathname;
        const hashWithExt = v.hashWithExt;
        const htmlDesc = v.htmlDesc;
        const dupwith = v.dupwith;
        // const item = `<li><a href="${hashWithExt}">clean</a>|<a href="${hashWithExt}#${desc}">${pathname.replace(startPath.replaceAll("/","\\"), "")}</a>&nbsp;</li>`;
        // const item = `<li><a href="/delete?path=${pathname}">Delete</a>|<a onMouseOver="this.style.color='red'" onmouseout="this.style.color=''" href="${hashWithExt}#~:~hint=${desc}" title="${dupwith}">${pathname}</a></li>`;
        let escapedPathname = pathname.replaceAll("\\", "^");
        let escapedPathnameDupwith = dupwith.pathname.replaceAll("\\", "^");
        const item = `<li><a href="javascript:if(confirm('确实要删除?${pathname}'))location='/delete?path=${escapedPathname}'">Delete</a>|<a onMouseOver="this.style.color='red'" onmouseout="this.style.color=''" target="_blank" href="${hashWithExt}#~:~hint=${htmlDesc}">${pathname}</a>-><a href="javascript:if(confirm('确实要删除?${dupwith.pathname}'))location='/delete?path=${escapedPathnameDupwith}'">Delete</a>|<a onMouseOver="this.style.color='red'" onmouseout="this.style.color=''" target="_blank" href="${dupwith.hashWithExt}#~:~hint=${dupwith.htmlDesc}">${dupwith.pathname}</a></li>`;
        content += item;
    });
    res.send(`
<html>
<li><a href="/">Home</a>&nbsp;duplicated items count: ${duplicatedList.length}</li>
${content}
</html>
    `);
});

// 地址格式：http://localhost:3010/delete?path=f6f0d83ed776101d9842ee0d43ee81507d63baa2.html#~:~hint=(1%20%E5%B0%81%E7%A7%81%E4%BF%A1%20
router.get('/delete',  (req, res, next) => {
    const filepath = req.query["path"];
    const pathname = map.get(filepath).pathname;
    // let filepath_decode = filepath.replaceAll("^", "\\");
    // console.log(filepath_decode);
    try {
        fs.unlinkSync(pathname);
        res.contentType("application/json")
        res.send(`
{ 
"Result": "success",
"Path": "${pathname}"
}
    `);
    } catch(e) {
      res.contentType("application/json")
      res.send(`
{ 
"Result": "failed",
"Path": "${pathname}"
"Reason": "${e}"
}
        `)
    }
});
router.get('/desc', (req, res, next) => {
    let content = "";
    let contentList = [];
    map.forEach((v, k, map) => {
        contentList.push(v);
    });
    contentList.sort(function(a,b){
        /*
        * return b-a; —> 降序排序
        * return a-b; —> 升序排列
        */
        return b.ctime-a.ctime;
    }); //括号里不写回调函数则默认按照字母逐位升序排列
  contentList.forEach((v, i, ary) => {
    const pathname = v.pathname;
    const hashWithExt = v.hashWithExt;
    const desc = v.htmlDesc;
    // const item = `<li><a href="${hashWithExt}">clean</a>|<a href="${hashWithExt}#${desc}">${pathname.replace(startPath.replaceAll("/","\\"), "")}</a>&nbsp;</li>`;
    const item = `<li><a href="${hashWithExt}">clean</a>|<a href="${hashWithExt}#~:~hint=${desc}">${pathname}</a>&nbsp;</li>`;
    content += item;
  });
  res.send(`
<html>
<li><a href="/">Home</a></li>
${content}
</html>
    `);
});

router.get('/latest', (req, res, next) => {
  let content = "";
  let contentList = [];
  map.forEach((v, k, map) => {
    contentList.push(v);
  });
  contentList.sort(function(a,b){
    /*
    * return b-a; —> 降序排序
    * return a-b; —> 升序排列
    */
    return b.ctime-a.ctime;
  }); //括号里不写回调函数则默认按照字母逐位升序排列
  res.redirect(`${contentList[0].hashWithExt}#~:~hint=${contentList[0].htmlDesc}`);
});

app.get('/videoplayer/*', handlers.mediaHtml5DataHandler);

app.use(router);
app.use(function(req, res, next) {
    console.error(`requesting ${req.path}`)
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// app.use(function (err, req, res, next) {
//     if (err.name === 'UnauthorizedError') {
//         //  这个需要根据自己的业务逻辑来处理（ 具体的err值 请看下面）
//         res.status(401).send('invalid token...');
//     }
// });
/// error handlers

// development error handler
// will print stacktrace
if (!isProduction) {
  app.use(function(err, req, res, next) {
    console.log(err.stack);

    res.status(err.status || 500);

    res.json({'errors': {
      message: err.message,
      error: err
    }});
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.json({'errors': {
    message: err.message,
    error: {}
  }});
});



// finally, let's start our server...
var server = app.listen( port || 3001, function(){
  console.log('Listening on port ' + server.address().port);
});
